## Декораторы функций

Декорирование представляет собой способ указания управляющего или дополняющего кода для функций и классов. 

Сами декораторы принимают вид вызываемых объектов (например, функций), обрабатывающих другие вызываемые объекты.

Используя вложенную функцию `wrapper` и механизм замыканий, будем вызывать переданную функцию func внутри вложенной функции `wrapper`:

```python
def func_decorator(func):
    def wrapper():
        print("--- перед вызовом функции ---")
        func()
        print("--- после вызова функции ---")

    return wrapper
```

Далее, для примера, объявим некоторую функцию:
```python
def some_func():
    print("Вызов функции some_func")
```


И передадим ссылку на нее функции `func_decorator`:

```python
f = func_decorator(some_func)
```

В результате, переменная `f` будет ссылаться на вложенную функцию wrapper и при ее вызове:

> f()

соответственно запустится `wrapper()`, а она, в свою очередь, запустит переданную функцию `func()`, то есть, `some_func()`.

То есть, декоратор – это функция, которая принимает ссылку на другую функцию и расширяет ее функциональность за счет вложенной функции.

Часто, чтобы не создавать новые имена функций, вместо имени f используют то же самое имя функции:

```python
some_func = func_decorator(some_func)
some_func()
```

И у нас получается, словно, мы модифицировали существующую функцию. В этом и заключается смысл декоратора – наполнить уже существующую функцию дополнительным функционалом.

Однако, если сейчас в нашу функцию `some_func()` добавить хотя бы один параметр:

```python
def some_func(title):
    print(f"title = {title}")

```
А, затем, вызвать ее с одним аргументом:

```python
some_func = func_decorator(some_func)
some_func("Python навсегда!")
```

то увидим ошибку, так как внутренняя функция `wrapper()` прописана без параметров. А именно на нее и ссылается сейчас `some_func`.

В самом простом варианте, мы, конечно, может просто добавить этот параметр для `wrapper()`:

```python
def func_decorator(func):
    def wrapper(title):
        print("------ что-то делаем перед вызовом функции ------")
        func(title)
        print("------ что-то делаем после вызова функции ------")
 
    return wrapper
```

И теперь программа будет запускаться без ошибок. 

Но, очевидно, она перестанет работать, если число параметров функции `func()` изменится. 

Как описать универсальную реализацию декоратора для функций с разным числом фактических и формальных параметров? Конечно, для этого нужно определить вложенную функцию, принимающую произвольное число аргументов, следующим образом:

```python
def func_decorator(func):
    def wrapper(*args, **kwargs):
        print("------ что-то делаем перед вызовом функции ------")
        func(*args, **kwargs)
        print("------ что-то делаем после вызова функции ------")
 
    return wrapper
```

А, затем, переданные значения распаковываются и передаются функции func(). Получаем универсальную реализацию декоратора.

И, действительно, если теперь добавить еще один параметр в функцию `some_func`:

```python
def some_func(title, tag):
    print(f"title = {title}, tag = {tag}")
```
То нам достаточно будет при вызове декоратора передать эти два аргумента:

```python
some_func("Python навсегда!", 'h1')
```
Сам декоратор остается без изменений.

И, наконец, последний штрих. Сама по себе функция some_func() может возвращать какие-либо значения, например, помимо вывода сообщения в консоль, она еще будет возвращать сформированную строку:

```python
def some_func(title, tag):
    print(f"title = {title}, tag = {tag}")
    return f"<{tag}>{title}</{tag}>"
```

Но, при попытке получить сейчас это значение:
```python
some_func = func_decorator(some_func)
res = some_func("Python навсегда!", 'h1')
print(res)
```
Увидим `None`. 

И я, думаю, вы прекрасно понимаете почему? Да, это потому, что вложенная функция `wrapper()` ничего не возвращает. Исправим это и сделаем так, чтобы она возвращала значение функции `func()`:

```python
def func_decorator(func):
    def wrapper(*args, **kwargs):
        print("------ что-то делаем перед вызовом функции ------")
        res = func(*args, **kwargs)
        print("------ что-то делаем после вызова функции ------")
        return res
 
    return wrapper
```

Вот теперь получили окончательную версию универсального декоратора функции и при выполнении программу видим не только его работу, но и возвращенное функцией значение.

Возможно, на протяжении всего этого урока вы задаете себе вопрос – зачем это надо? 

Где применяется? Приведу такой пример. 

Предположим, мы хотим протестировать различные функции на скорость их работы. 


Пример алгоритм последовательности Фибоначчи.
Последовательность чисел Фибоначчи задается рекуррентным соотношением:
```
F(1) = 1
F(2) = 1
F(n) = F(n–2) + F(n–1), при n >2, где n – натуральное число.
```
Чему равно `n`-е число в последовательности Фибоначчи?



Для этого возьмем функцию, которая реализует рекурсивный (мы ее с вами делали на одном из прошлых занятий):

```python
def fib_recurs(n)::
    # самостоятельно
```
И перед ней опишем декоратор-тестировщик:
```python
def test_time(fn):
    def wrapper(*args, **kwargs):
        st = time.time()
        res = fn(*args, **kwargs)
        dt = time.time() - st
        print(f"Время работы: {dt} сек")
        return res
 
    return wrapper
```

Мы здесь замеряем время работы функции и выводим его в консоль. Чтобы воспользоваться функцией time, импортируем этот модуль:
```python
import time
# и декорируем функцию get_delta:

get_delta = test_time(fib_recurs)
res = get_delta(30)
print(res)
```

После запуска программы видим время ее выполнения и возвращенный результат.

Мало того, если у нас будет еще какая-либо функция, например, тот же алгоритм , но с быстрой реализацией:
```python
def fib_cicle(n):
  ...
```
То мы легко можем применить тот же самый декоратор для тестирования скорости работы этой второй функции:

```python
get_delta = test_time(fib_cicle)
res = get_delta(30)
print(res)
```

Получается, что мы через декоратор `test_time` определили универсальный алгоритм для тестирования скорости работы любых функций. 

И это очень удобно!

И, наконец, последнее, о чем я хочу рассказать на этом занятии. Декораторы можно навешивать (применять) к функциям с помощью специального значка @. 

То есть, вместо строчек:

```python
get_delta = test_time(fib_recurs)
get_delta = test_time(fib_cicle)
```

достаточно перед объявлением функций прописать `@test_time`.

В результате, эти функции, как бы, «обертываются» нашим декоратором test_time и расширяются его функционалом.

Если мы теперь запустим программу, то увидим тот же самый результат. 

А вот если у какой-либо функции уберем эту строчку, то будет обычный вызов без оценки скорости работы. Фактически, запись:

```python
@test_time
def fib_recurs(n):
    ...
# эквивалентна строчке:

get_delta = test_time(fib_recurs)
```

но со значком «собачка» программа выглядит, на мой взгляд, понятнее, да и записывать это декорирование проще.
но декорировать рекурсивные функции - не гут
```
Время работы: 0.0 сек
Время работы: 0.0 сек
Время работы: 0.0 сек
Время работы: 0.0 сек
```

На практике, как правило, используют именно этот второй вариант.

Выражаясь кратко, декораторы предлагают способ вставки автоматически запускаемого кода в конце операторов определения функций и классов — в конце `def` для декораторов функций и в конце `class` для декораторов классов. 

Такой код может иcполнять множество ролей.

```python
user = "admin"

def access(func):
    def wrapper():
        if user == "admin":
            return func()
        else:
            print("access denied")
    return wrapper

@access
def read_db():
    print("reading data base")


read_db()

```



Декораторы классов делают повторное привязывание имен во время определения классов, предоставляя уровень логики, который может управлять классами или экземплярами, созданными при последующих обращениях к классам.

```python
@test_time
class MyClass:
    def fib_cicle(self, n):
        a, b = 1, 1
        i = 2
        while i < n:
            a, b = b, a + b
            i += 1
        return b

#
obj = MyClass()
res = obj.fib_cicle(20)


print(res)
```


Надеюсь, вы поняли, как они работают и для чего применяются?
```python
@property
@dataclass
@staticmethod
@classmethod
```
Добавив метод `__call__` в класс, его можно превратить в вызываемый объект. 

А поскольку декоратор — это всего лишь функция, то есть, вызываемый объект, класс можно превратить в декоратор с помощью функции `__call__`.

Лучше всего разобрать это на примере.
```python
class Decorator:
    def __init__(self, func):
        print('> Класс Decorator метод __init__')
        self.func = func

    def __call__(self):
        print('> перед вызовом класса...', self.func.__name__)
        self.func()
        print('> после вызова класса')

@Decorator
def wrapped():
    print('функция wrapped')

print('>> старт')
wrapped()
print('>> конец')

```

Немного углубимся в эту тему и начнем с декораторов, которым можно дополнительно передавать аргументы.

### Получение параметров функции в декораторе


Декоратор может перехватывать передаваемые в функцию аргументы:


```python
# определение функции декоратора
def check(input_func):    
    def output_func(*args):      # через *args получаем значения параметров оригинальной функции
        input_func(*args)                # вызов оригинальной функции
    return output_func     # возвращаем новую функцию
 
# определение оригинальной функции
@check
def print_person(name, age):
    print(f"Name: {name}  Age: {age}")
 
# вызов оригинальной функции
print_person("Tom", 38)
```
Здесь функция `print_person()` принимает два параметра: `name (имя)` и `age (возраст)`. 

К этой функции применяется декоратор `check()`

В декораторе `check` возвращается локальная функция `output_func()`, которая принимает некоторый набор значений в виде параметра *args - это те значения, которые передаются в оригинальную функцию, к которой применяется декоратор. 

То есть в данном случае *args будет содержать значения параметров name и age.
```python
def check(input_func):    
    def output_func(*args):      # через *args получаем значения параметров функции input_func
```

Здесь просто передаем эти значения в оригинальную функцию:

В итоге в данном получим следующий консольный вывод
```
Name: Tom  Age: 38
```

Но что, если в функцию `print_person` будет передано какое-то недопустимое значение, например, отрицательный возраст?

Одним из преимуществ декораторов как раз является то, что мы можем проверить и при необходимости модифицировать значения параметров. 

Например:
```python

# определение функции декоратора
def check(input_func):    
    def output_func(*args):
        name = args[0]
        age = args[1]           # получаем значение второго параметра
        if age < 0: age = 1     # если возраст отрицательный, изменяем его значение на 1
        input_func(name, age)   # передаем функции значения для параметров
    return output_func
 
# определение оригинальной функции
@check
def print_person(name, age):
    print(f"Name: {name}  Age: {age}")
 
# вызов оригинальной функции
print_person("Tom", 38)
print_person("Bob", -5)

```

`args` фактически представляет набор значений, и, используя индексы, мы можем получить значения параметров по позиции и что-то с ними сделать. 

Так, здесь, если значение возраста меньше 0, то устанавливаем 1. 

Затем передаем эти значения в вызов функции. В итоге здесь получим следующий вывод:

```
Name: Tom  Age: 38
Name: Bob  Age: 1
```

Получение результата функции
Подобным образом можно получить результат функции и при необходимости изменить его:

```python

# определение функции декоратора
def check(input_func):    
    def output_func(*args):
        result = input_func(*args)   # передаем функции значения для параметров
        if result < 0: result = 0   # если результат функции меньше нуля, то возвращаем 0
        return result
    return output_func
 
# определение оригинальной функции
@check
def sum(a, b):
    return a + b
 
# вызов оригинальной функции
result1 = sum(10, 20)
print(result1)          # 30
 
result2 = sum(10, -20)
print(result2)          # 0
```

Здесь определена функция `sum()`, которая возвращает сумму чисел. 

В декораторе `check` проверяем результат функции и для простоты, если он меньше нуля, то возвращаем 0.

Консольный вывод программы:

```
30
0
```
